Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Remove unnecessary use of dynamics #652

Merged
merged 18 commits into from
Oct 12, 2018
Merged

Remove unnecessary use of dynamics #652

merged 18 commits into from
Oct 12, 2018

Conversation

tommymonk
Copy link
Contributor

The use of dynamic throughout the code made it difficult to safely make a change relating to #650
Dynamics are terrible for hiding refactor mistakes until run-time.

I've plumbed through JObject for response and JToken for method interface, this clarity allowed me to do a lot of streamlining of the response processing and in some places identify where we are serialize/de-serialize the same data many times.

I didn't want to add to the string debt so I pulled out a large amount of the key strings into a Consts class to help with quality and refactor and reduce risk of future mistake.

As discussed in #650 it seemed like a sensible idea to remove the 'dynamic' or 'object overloads of methods like EvaluateFunctionAsync which are a cause of confusion, however if someone wants to use the type 'object' they will now get the correct types serialized for numerics.

All of the existing tests are passing for me, I wanted to add extra tests to cover #650 but wasn't sure which test class to use, maybe you could guide me ?

Copy link
Member

@Meir017 Meir017 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

basically when the response isn't used you don't need to specify the type (<object>) just use the JObject overload

lib/PuppeteerSharp/Constants.cs Outdated Show resolved Hide resolved
lib/PuppeteerSharp/CDPSession.cs Show resolved Hide resolved
"document.cookie = 'doSomethingOnlyOnce=true; expires=Fri, 31 Dec 9999 23:59:59 GMT'");
}

using (var browser2 = await Puppeteer.LaunchAsync(options, TestConstants.LoggerFactory))
{
var page2 = await browser2.NewPageAsync();
await page2.GoToAsync(TestConstants.EmptyPage);
Assert.Equal("doSomethingOnlyOnce=true", await page2.EvaluateExpressionAsync("document.cookie"));
Assert.Equal("doSomethingOnlyOnce=true", await page2.EvaluateExpressionAsync<object>("document.cookie"));
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this should probably be EvaluateExpressionAsync<string>

@@ -156,15 +156,15 @@ public async Task UserDataDirOptionShouldRestoreCookies()
{
var page = await browser.NewPageAsync();
await page.GoToAsync(TestConstants.EmptyPage);
await page.EvaluateExpressionAsync(
await page.EvaluateExpressionAsync<object>(
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can this use the JObject overload without specifying <object>?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As I mentioned in the PR description, I thought by removing the default object and dynamic methods we would make users think about what they want, I can add back methods defaulting to object though ?

}

using (var browser2 = await Puppeteer.LaunchAsync(options, TestConstants.LoggerFactory))
{
var page2 = await browser2.NewPageAsync();
await page2.GoToAsync(TestConstants.EmptyPage);
Assert.Equal("hello", await page2.EvaluateExpressionAsync("localStorage.hey"));
Assert.Equal("hello", await page2.EvaluateExpressionAsync<object>("localStorage.hey"));
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this should be <string> not <object>

@@ -402,7 +402,7 @@ public async Task ShouldSelectTheTextWithMouse()
await Page.FocusAsync("textarea");
const string text = "This is the text that we are going to try to select. Let's see how it goes.";
await Page.Keyboard.TypeAsync(text);
await Page.EvaluateExpressionAsync("document.querySelector('textarea').scrollTop = 0");
await Page.EvaluateExpressionAsync<object>("document.querySelector('textarea').scrollTop = 0");
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can this use the JObject overload without specifying <object> ?

@@ -448,7 +448,7 @@ public async Task ShouldFireContextmenuEventOnRightClick()
public async Task ShouldSetModifierKeysOnClick()
{
await Page.GoToAsync(TestConstants.ServerUrl + "/input/scrollable.html");
await Page.EvaluateExpressionAsync("document.querySelector('#button-3').addEventListener('mousedown', e => window.lastEvent = e, true)");
await Page.EvaluateExpressionAsync<object>("document.querySelector('#button-3').addEventListener('mousedown', e => window.lastEvent = e, true)");
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can this use the JObject overload without specifying <object> ?

@@ -476,7 +476,7 @@ public async Task ShouldSpecifyRepeatProperty()
{
await Page.GoToAsync(TestConstants.ServerUrl + "/input/textarea.html");
await Page.FocusAsync("textarea");
await Page.EvaluateExpressionAsync("document.querySelector('textarea').addEventListener('keydown', e => window.lastEvent = e, true)");
await Page.EvaluateExpressionAsync<object>("document.querySelector('textarea').addEventListener('keydown', e => window.lastEvent = e, true)");
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can this use the JObject overload without specifying <object> ?

@@ -505,7 +505,7 @@ public async Task ShouldClickLinksWhichCauseNavigation()
public async Task ShouldTweenMouseMovement()
{
await Page.Mouse.MoveAsync(100, 100);
await Page.EvaluateExpressionAsync(@"{
await Page.EvaluateExpressionAsync<object>(@"{
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can this use the JObject overload without specifying <object> ?

@@ -580,7 +580,7 @@ public async Task ShouldTypeAllKindsOfCharacters()
public async Task ShouldSpecifyLocation()
{
await Page.GoToAsync(TestConstants.ServerUrl + "/input/textarea.html");
await Page.EvaluateExpressionAsync(@"{
await Page.EvaluateExpressionAsync<object>(@"{
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can this use the JObject overload without specifying <object> ?

Copy link
Member

@kblok kblok left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's make this change first so we get an smaller PR to review.

@@ -17,7 +17,7 @@ public static async Task Main(string[] args)
using (var browser = await Puppeteer.LaunchAsync(options))
using (var page = await browser.NewPageAsync())
{
await page.EvaluateFunctionAsync("_dumpioTextToLog => console.log(_dumpioTextToLog)", args[0]);
await page.EvaluateFunctionAsync<object>("_dumpioTextToLog => console.log(_dumpioTextToLog)", args[0]);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We need to support "void" functions. That function won't return a value. I don't think we have to force the user to pick a Type he won't need.

This comment applies to many changes here. If we don't need a result, we don't need a generic.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed

Copy link
Member

@Meir017 Meir017 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this should return Task<JToken>

/// <summary>
/// Executes a script in browser context
/// </summary>
/// <param name="script">Script to be evaluated in browser context</param>
/// <remarks>
/// If the script, returns a Promise, then the method would wait for the promise to resolve and return its value.
/// </remarks>
/// <seealso cref="EvaluateFunctionAsync(string, object[])"/>
/// <seealso cref="EvaluateFunctionAsync{T}(string, object[])"/>
/// <seealso cref="EvaluateExpressionHandleAsync(string)"/>
/// <returns>Task which resolves to script return value</returns>
public Task<object> EvaluateExpressionAsync(string script)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this should return Task<JToken>

@kblok
Copy link
Member

kblok commented Sep 28, 2018

@tommymonk there are still some tests that need to be reverted. On the other hand, this PR is conflicted. Could you resolve the conflicts? So the CI works on this PR?

BTW, thanks for the calories burned on this PR!

@tommymonk
Copy link
Contributor Author

I'm almost finished on the overloads the the Evaluate* methods but I've got test failures because the JToken now returned can 'empty' rather than null as confirmed by it's HasValues property. Would you rather I check for the HasValues property and return null, or task the calling code (tests and users) with checking this property ?

@kblok
Copy link
Member

kblok commented Sep 28, 2018

@tommymonk now the Evaluate* with not generic returns a Task<object>. I would by changing that to Task. So if you use EvaluateFunctionAsync("") we assume that you don't need the value. If you do need it, you should do EvaluateFunctionAsync<Foo>("").

We could also play smart and check if the user wants the raw JSON token EvaluateFunctionAsync<JToken>("") so if T is JToken we return the JSON object.

@tommymonk
Copy link
Contributor Author

tommymonk commented Sep 28, 2018

Unfortuantely I can't have overloads of these methods that return nothing (Task) as well as Task<JToken> because of course C# doesn't allow this with the same method signature.
I have chosen to go with Task<JToken> because then at least the caller can choose to not even acknowledge the return value

@tommymonk
Copy link
Contributor Author

@kblok rebased atop your commits today

Copy link
Member

@kblok kblok left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looking good, Getting rid of dynamics was something we always wanted to do and it's quite a big change but it worth the effort.

@@ -89,7 +89,8 @@ public async Task ShouldClickWrappedLinks()
public async Task ShouldClickOnCheckboxInputAndToggle()
{
await Page.GoToAsync(TestConstants.ServerUrl + "/input/checkbox.html");
Assert.Null(await Page.EvaluateExpressionAsync("result.check"));
var foo = await Page.EvaluateExpressionAsync("result.check");
Assert.Null(foo);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's undo this change we don't need it.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Forgot to take it out when debugging

@@ -13,25 +13,25 @@ public class BoundingBox : IEquatable<BoundingBox>
/// The x coordinate of the element in pixels.
/// </summary>
/// <value>The x.</value>
[JsonProperty("x")]
[JsonProperty(Constants.X)]
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's keep JsonProperties as strings it doesn't add much value to use constants here.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You're suggesting undoing all the work I put into removing strings scattered throughout the code ?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, sorry about that. It's not something I think we need to do, and it's out of the scope of removing dynamics.

lib/PuppeteerSharp/Browser.cs Outdated Show resolved Hide resolved
lib/PuppeteerSharp/Constants.cs Outdated Show resolved Hide resolved
@tommymonk
Copy link
Contributor Author

tommymonk commented Sep 28, 2018

Is there a reason that you don't just use BaristaLabs to generate all your poco objects for each of the devtools APIs, seems like this would take care of Response types you're wanting ?

https://github.com/BaristaLabs/chrome-dev-tools-generator

@kblok
Copy link
Member

kblok commented Sep 28, 2018

@tommymonk, basically we were growing little by little not even knowing that this project would get any interest. We started with dynamics, and then we began moving to classes, adding small classes as we needed.
Now I have smart people like you suggesting apps I never heard before 😂
I will take a look at that app.

Copy link
Member

@kblok kblok left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looking good

@@ -72,7 +73,7 @@ public async Task ShouldBeAbleToDetachSession()
var client = await Page.Target.CreateCDPSessionAsync();
await client.SendAsync("Runtime.enable");
var evalResponse = await client.SendAsync("Runtime.evaluate", new { expression = "1 + 2", returnByValue = true });
Assert.Equal(3, evalResponse.result.value.ToObject<int>());
Assert.Equal(3, evalResponse["result"]["value"].ToObject<int>());
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just thinking about an example. Would a user be able to do this?

dynamic evalResponse = await client.SendAsync<ExpandoObject>("Runtime.evaluate", new { expression = "1 + 2", returnByValue = true });
Assert.Equal(3, evalResponse.result.value.ToObject<int>());

Copy link
Contributor Author

@tommymonk tommymonk Sep 29, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Even easier;

dynamic evalResponse = await client.SendAsync("Runtime.evaluate", new { expression = "1 + 2", returnByValue = true });
Assert.Equal(3, evalResponse.result.value.ToObject<int>());

Or

var evalResponse = await client.SendAsync("Runtime.evaluate", new { expression = "1 + 2", returnByValue = true });
Assert.Equal(3, evalResponse.SelectToken("result.value"));

lib/PuppeteerSharp/CDPSession.cs Outdated Show resolved Hide resolved
lib/PuppeteerSharp/Connection.cs Outdated Show resolved Hide resolved
lib/PuppeteerSharp/Constants.cs Outdated Show resolved Hide resolved
lib/PuppeteerSharp/ExecutionContext.cs Outdated Show resolved Hide resolved
@@ -1,30 +1,31 @@
using System.Collections.Generic;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same here

@@ -0,0 +1,21 @@
using Newtonsoft.Json.Linq;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's move this to the Helper folder

@@ -1,39 +1,39 @@
using System.Collections.Generic;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's reduce the changelog here

@@ -1,16 +1,16 @@
using System;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's reduce the changelog

lib/PuppeteerSharp/Response.cs Outdated Show resolved Hide resolved
@tommymonk
Copy link
Contributor Author

Removed line-ending changes to the files mentioned, merged in your master changes.

Copy link
Member

@kblok kblok left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I love this progress. Great job!

lib/PuppeteerSharp/CDPSession.cs Outdated Show resolved Hide resolved
lib/PuppeteerSharp/Dialog.cs Outdated Show resolved Hide resolved
lib/PuppeteerSharp/ElementHandle.cs Show resolved Hide resolved
lib/PuppeteerSharp/ExecutionContext.cs Outdated Show resolved Hide resolved
lib/PuppeteerSharp/ExecutionContext.cs Outdated Show resolved Hide resolved
lib/PuppeteerSharp/Frame.cs Outdated Show resolved Hide resolved
lib/PuppeteerSharp/Frame.cs Outdated Show resolved Hide resolved
lib/PuppeteerSharp/Input/Mouse.cs Outdated Show resolved Hide resolved
lib/PuppeteerSharp/Page.cs Outdated Show resolved Hide resolved
@tommymonk
Copy link
Contributor Author

Just wanted to confirm you aren't waiting on me for any further rework ?

@kblok
Copy link
Member

kblok commented Oct 10, 2018

@tommymonk cool I didn't know it was ready again.

lib/PuppeteerSharp/Dialog.cs Outdated Show resolved Hide resolved
lib/PuppeteerSharp/Frame.cs Outdated Show resolved Hide resolved
@kblok
Copy link
Member

kblok commented Oct 10, 2018

Besides my Evaluate* concerns, I think this is almost ready to go.
@Meir017 would you mind taking a look at it?

@tommymonk would you mind writing a Breaking Change report so we tell the users what it's changing for them and what they would need to change to get their app working?

@kblok
Copy link
Member

kblok commented Oct 10, 2018

@Meir017 let's try to merge this before any other PR, so we don't conflict our friend @tommymonk

@tommymonk
Copy link
Contributor Author

tommymonk commented Oct 10, 2018

I'm bound to have forgotten something, but will update this comment with your feedback.

Should we mention the change of Evaluate* return type changing from dynamic to JToken because the current type being boxed as dynamic is JToken, though ?
Same goes for Response.JsonAsync and CDPSession.SendAsync I guess ?

Use of evaluative methods that previously resolved to a JavaScript number will now return a more appropriate .NET type as discussed in #650

@kblok
Copy link
Member

kblok commented Oct 11, 2018

 @tommymonk think about the code that a user might need to change, so we add that to our release notes.

@kblok kblok merged commit d4c5d05 into hardkoded:master Oct 12, 2018
@kblok
Copy link
Member

kblok commented Oct 12, 2018

@tommymonk 💥

@tommymonk
Copy link
Contributor Author

Thanks

@tommymonk tommymonk deleted the features/remove-dynamic-overuse branch October 12, 2018 11:37
@tommymonk
Copy link
Contributor Author

tommymonk commented Oct 12, 2018

I believe that I've found a bug introduced by my PR that isn't covered by test.

await new BrowserFetcher().DownloadAsync(BrowserFetcher.DefaultRevision);

var browser = await new Launcher().LaunchAsync(new LaunchOptions
{
    Headless = true,
});

var page = await browser.NewPageAsync();

var result = await page.EvaluateExpressionAsync("var str = 'asdf'; str;"); // result is null

browser.Dispose();

The return type of EvaluateExpressionAsync isn't specified, the object is string and it return null

Its cause because of this line of code in ExecutionContext;
return result is JToken token && !token.HasValues ? default : result;

Instead of just checking HasValues it needs to use a smarter test like the top answer here

public static bool IsNullOrEmpty(this JToken token)
{
    return (token == null) ||
           (token.Type == JTokenType.Array && !token.HasValues) ||
           (token.Type == JTokenType.Object && !token.HasValues) ||
           (token.Type == JTokenType.String && token.ToString() == String.Empty) ||
           (token.Type == JTokenType.Null);
}

@kblok
Copy link
Member

kblok commented Oct 12, 2018

@tommymonk go for it an add a test :)

@tommymonk
Copy link
Contributor Author

My branch is gone now so I'll create a new one and new PR ?

@kblok
Copy link
Member

kblok commented Oct 12, 2018

@tommymonk yes

@tommymonk
Copy link
Contributor Author

I'll get on it, can you suggest where is appropriate fore new tests to go ?

@kblok
Copy link
Member

kblok commented Oct 12, 2018

@tommymonk I would go with PageTests.EvaluateTests.ShouldWorkWithoutGenerics()

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

3 participants